#!/usr/bin/env python
# Copyright (C) 2007 by Xyhthyx <xyhthyx@gmail.com>

# This file is part of Parcelle.

# Parcelle is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.

# Parcelle is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.

# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

import os
import re
import sys
import gtk
import atexit
import gobject
import datetime
import webbrowser
try:
	import xml.dom.minidom
	import xml.dom.ext
except ImportError:
	print 'Pease install the python-xml module.'
	sys.exit(1)
import common
from control.trayicon import TrayIcon
from control.about import AboutDialog
from control.preferences import Preferences
from widgets.preferences import PreferencesWidgets

# Main class which controls the main window of
# the application
class Parcelle:
	
# ---------------------------------------------------------------------------
# 	Start constructor
# ---------------------------------------------------------------------------
	
	def __init__(self, widgets):
		
		# Get widgets, create clipboard, page reference and start tray icon
		self.options = {'remember': 300, 'capture': 'Clipboard', 'append': True, 'warning': True, 'browse': True, 'hyperlinks': False}
		self.widgets = widgets
		self.tray = TrayIcon(self)
		self.clipboard = gtk.clipboard_get(gtk.gdk.SELECTION_CLIPBOARD)
		self.clipboard_text = self.clipboard.wait_for_text()
		self.last_entry = None
		self.page = self.widgets.notebook.get_current_page()
		
		# Create buffers
		self.bfrClip = gtk.TextBuffer()
		self.bfrEdit = gtk.TextBuffer()
		self.widgets.txtClipboard.set_buffer(self.bfrClip)
		self.widgets.txtEdit.set_buffer(self.bfrEdit)
		
		# Create treeview items
		self.tree_iter = None
		self.tree_list = gtk.ListStore(str, str)
		self.widgets.trParcelle.set_model(self.tree_list)
		self.tree_selection = self.widgets.trParcelle.get_selection()
		self.tree_selection.set_mode(gtk.SELECTION_BROWSE)
		
		# Make about window have clickable link
		gtk.about_dialog_set_url_hook(lambda d, l:webbrowser.open_new(l))
		
		# Build treeview and get options
		self.build()
		
		# -------------------------------
		#  Connect signals
		# -------------------------------
		
		# Clipboard events:
		self.handler = self.clipboard.connect('owner-change', self.clipboard_new_text)
		
		# Form events:
		self.widgets.frmMain.connect('delete-event', self.on_main_delete)
		
		# Treeview related events:
		self.widgets.trParcelle.connect('cursor-changed', lambda t:self.update())
		self.widgets.trParcelle.connect('unselect-all', lambda t:self.update())
		self.widgets.trParcelle.connect('button-press-event', self.popup_menu)
		self.widgets.trParcelle.connect('key-press-event', self.on_key_pressed)
		self.widgets.trParcelle.connect('row-activated', self.on_row_activated)
		self.tree_selection.connect('changed', lambda t:self.update())
		self.widgets.notebook.connect('switch-page', self.page_changed)
		self.widgets.tglItems.connect('toggled', self.on_items_toggled)
		self.widgets.tglClipboard.connect('toggled', self.on_clipboard_toggled)
		
		# Menu events:
		self.widgets.mnuOpen.connect('activate', self.on_open_clicked)
		self.widgets.mnuSave.connect('activate', self.on_saveas_clicked)
		self.widgets.mnuHide.connect('activate', lambda b:self.widgets.frmMain.hide())
		self.widgets.mnuQuit.connect('activate', self.on_quit_clicked)
		self.widgets.mnuEdit.connect('activate', self.on_edit_clicked)
		self.widgets.mnuCopy.connect('activate', self.on_copy_clicked)
		self.widgets.mnuDelete.connect('activate', self.on_delete_clicked)
		self.widgets.mnuClear.connect('activate', self.on_clear_clicked)
		self.widgets.mnuPreferences.connect('activate', self.on_preferences_clicked)
		self.widgets.mnuAbout.connect('activate', self.on_about_clicked)
		
		# Toolbar and button events:
		self.widgets.btnPreferences.connect('clicked', self.on_preferences_clicked)
		self.widgets.btnOpen.connect('clicked', self.on_open_clicked)
		self.widgets.btnSave.connect('clicked', self.on_saveas_clicked)
		self.widgets.btnEdit.connect('clicked', self.on_edit_clicked)
		self.widgets.btnCopy.connect('clicked', self.on_copy_clicked)
		self.widgets.btnDelete.connect('clicked', self.on_delete_clicked)
		self.widgets.btnClear.connect('clicked', self.on_clear_clicked)
		self.widgets.btnHide.connect('clicked', lambda b:self.widgets.frmMain.hide())
		self.widgets.btnQuit.connect('clicked', self.on_quit_clicked)
		self.widgets.btnClipboardSave.connect('clicked', self.on_save_clicked)
		self.widgets.btnClipboardBack.connect('clicked', lambda b:self.widgets.notebook.set_current_page(0))
		self.widgets.btnEditSave.connect('clicked', self.on_save_clicked)
		self.widgets.btnEditCancel.connect('clicked', lambda b:self.widgets.notebook.set_current_page(0))
		
		# Find related events:
		self.widgets.entFind.connect('activate', self.on_find_clicked)
		self.widgets.btnFind.connect('clicked', self.on_find_clicked)
 		self.widgets.btnFindClear.connect('clicked', self.on_find_clear_clicked)
		
		# Register at exit operations
		atexit.register(lambda:self.save_document())
		
		# Update and remove extra items in case Parcelle
		# was killed instead of properly shut down
		self.remove_extra()
		self.update()
	
	# Build treeview and get options
	def build(self):
		
		# Try to append the saved history items to the treeview
		if os.path.exists(common.PARCELLE_FILE):
			try:
				history = xml.dom.minidom.parse(common.PARCELLE_FILE)
				# Get the options
				options = history.getElementsByTagName('parcelle')
				try:
					self.options['remember'] = float(options[0].attributes['remember'].nodeValue)
				except:
					pass
				try:
					if str(options[0].attributes['capture'].nodeValue) == 'Primary':
						self.options['capture'] = 'Primary'
						self.clipboard = gtk.clipboard_get("PRIMARY")
				except:
					pass
				try:
					# For some reason directly typecasting with bool() always returns
					# True, so I have to do this instead
					if options[0].attributes['append'].nodeValue == 'False':
						self.options['append'] = False
				except:
					pass
				try:
					# Same as above
					if options[0].attributes['warning'].nodeValue == 'False':
						self.options['append'] = False
				except:
					pass
				try:
					# Same as above again
					if options[0].attributes['browse'].nodeValue == 'False':
						self.options['browse'] = False
				except:
					pass
				try:
					# Same as above again!!
					if options[0].attributes['hyperlinks'].nodeValue == 'True':
						self.options['hyperlinks'] = True
				except:
					pass
				
				# Append all the items
				items = history.getElementsByTagName('item')
				
				# Freeze tree from updating until it finishes adding all
				# the items so it doesn't lag
				self.widgets.trParcelle.freeze_child_notify()
				for item in items:
					self.tree_list.append([item.attributes['date'].nodeValue, item.childNodes[0].nodeValue])
				self.widgets.trParcelle.thaw_child_notify()
				# Make clipboard_text last entry from history
				if len(self.tree_list) > 0:
					row = self.tree_list[-1]
					self.clipboard_text = row[1]
					self.last_entry = row[1]
			except:
				dialog = gtk.MessageDialog(self.widgets.frmMain, 0, gtk.MESSAGE_INFO, gtk.BUTTONS_OK, 'An error occured restoring the previous session. Starting a new session...')
				icon = dialog.render_icon(gtk.STOCK_PASTE, gtk.ICON_SIZE_MENU)
				dialog.set_icon(icon)
				answer = dialog.run()
				dialog.destroy()
	
# ---------------------------------------------------------------------------
# 	Start treeview related callbacks and methods
# ---------------------------------------------------------------------------
	
	# Updates the selection iter and the current page among others
	def update(self):
		
		selected = self.tree_selection.get_selected()
		self.tree_iter = selected[1]
		self.page = self.widgets.notebook.get_current_page()
		self.widgets.lblCount.set_text(str(len(self.tree_list)) + '/' + str(int(self.options['remember'])))
	
	# Returns selected text
	def get_selected_text(self):
		
		self.update()
		if self.tree_iter:
			text = self.tree_list.get_value(self.tree_iter, 1)
		else:
			text = ''
		return text
	
	# Items toggled callback
	def on_items_toggled(self, toggle_button):
		
		if toggle_button.get_active():
			self.widgets.notebook.set_current_page(0)
	
	# Clipboard toggled callback
	def on_clipboard_toggled(self, toggle_button):
		
		if toggle_button.get_active():
			self.widgets.notebook.set_current_page(1)
	
	# Tab page changed callback
	def page_changed(self, notebook, page, page_num):
		
		self.update()
		# Items page
		if page_num == 0:
			self.widgets.tglItems.set_active(True)
			self.widgets.tglItems.set_sensitive(False)
			self.widgets.tglClipboard.set_active(False)
			self.widgets.tglClipboard.set_sensitive(True)
		# Clipboard contents page
		elif page_num == 1:
			if not self.clipboard_text:
				self.bfrClip.set_text('')
			else:
				self.bfrClip.set_text(self.clipboard_text)
			text = 'Editing ' + self.options['capture']
			self.widgets.lblClipboard.set_label(text)
			self.widgets.tglClipboard.set_active(True)
			self.widgets.tglClipboard.set_sensitive(False)
			self.widgets.tglItems.set_active(False)
			self.widgets.tglItems.set_sensitive(True)
		# Item editing page
		else:
			self.bfrEdit.set_text(self.get_selected_text())
			self.widgets.tglItems.set_active(False)
			self.widgets.tglItems.set_sensitive(True)
			self.widgets.tglClipboard.set_active(False)
			self.widgets.tglClipboard.set_sensitive(True)
	
	# Save button callback
	def on_save_clicked(self, widget):
		
		self.update()
		if self.page == 1:
			# Save changes to clipboard
			text = self.bfrClip.get_text(self.bfrClip.get_start_iter(), self.bfrClip.get_end_iter())
			if not text == self.clipboard_text:
				self.clipboard.set_text(text)
				self.clipboard_text = text
		elif self.page == 2:
			# Save changes to item
			text = self.bfrEdit.get_text(self.bfrEdit.get_start_iter(), self.bfrEdit.get_end_iter())
			self.tree_list.set_value(self.tree_iter, 0, datetime.datetime.now().strftime('%m/%d/%y %I:%M%p').lower())
			self.tree_list.set_value(self.tree_iter, 1, text)
		
		# Return to items page
		self.widgets.notebook.set_current_page(0)
	
# ---------------------------------------------------------------------------
# 	Start clipboard related methods
# ---------------------------------------------------------------------------
	
	# New clipboard text recieved
	def clipboard_new_text(self, clipboard, event):
		
		if clipboard.wait_is_target_available('application/x-gtk-text-buffer-rich-text') and event.selection == 'PRIMARY':
			return
		elif clipboard.wait_is_target_available('GTK_TEXT_BUFFER_CONTENTS') and event.selection == 'PRIMARY':
			return
		
		text = clipboard.wait_for_text()
		# Empty text check
		if not text or text == '' or text == self.last_entry:
			self.clipboard_text = text
			return
		else:
			
			# Hyperlink mode check, returning if not URL
			if self.options['hyperlinks'] and not self.check_url(text):
				self.clipboard_text = text
				return
			elif text == self.clipboard_text:
				# Text is duplicate
				self.clipboard_text = text
				return
			else:
				# Text is new, adding to list...
				self.tree_list.append([datetime.datetime.now().strftime('%m/%d/%y %I:%M%p').lower(), text])
				self.tray.recent.push(text)
				self.widgets.lblCount.set_text(str(len(self.tree_list)) + '/' + str(int(self.options['remember'])))
				self.save_document(common.PARCELLE_FILE)
				self.clipboard_text = text
				self.last_entry = text
	
# ---------------------------------------------------------------------------
# 	Start find and popup menu related callbacks and methods
# ---------------------------------------------------------------------------
	
	# Row double clicked
	def on_row_activated(self, treeview, path, view_column):
		
		if self.options['browse']:
			# Check if selected text is URL
			selected_text = self.get_selected_text()
			
			if self.check_url(selected_text):
				# Match found, launching default browser...
				webbrowser.open_new(selected_text)
			else:
				# Text not URL, editing item...
				self.on_edit_clicked()
			
		else:
			# Option turned off, editing item...
			self.on_edit_clicked()
	
	# Popup menu for right-clicks
	def popup_menu(self, widget, event):
		
		self.update()
		
		if event.button == 3 and self.tree_iter:
			
			menu = gtk.Menu()
			
			# Edit
			menu_item = gtk.ImageMenuItem(gtk.STOCK_EDIT)
			menu_item.connect('activate', self.on_edit_clicked)
			menu.append(menu_item)
			# Copy
			menu_item = gtk.ImageMenuItem(gtk.STOCK_COPY)
			menu_item.connect('activate', self.on_copy_clicked)
			menu.append(menu_item)
			# Delete
			menu_item = gtk.ImageMenuItem(gtk.STOCK_DELETE)
			menu_item.connect('activate', self.on_delete_clicked)
			menu.append(menu_item)
			# Clear
			menu_item = gtk.ImageMenuItem(gtk.STOCK_CLEAR)
			menu_item.connect('activate', self.on_clear_clicked)
			menu.append(menu_item)
			
			menu.show_all()
			menu.popup(None, None, None, event.button, event.time)
	
	# Find button callback
	def on_find_clicked(self, widget):
		
		self.update()
		if self.widgets.entFind.get_text() == '':
			return
		elif self.page != 0:
			return
		tmp = self.widgets.entFind.get_text()
		text = ''
		# Characters in search that need to be escaped
		escape = ['\\', '\'', '\"' '.', '^', '$', '*', '+', '?', '{', '}', '(', ')', '[', ']', '|', '#']
		# Check for characters to escape
		for letter in tmp:
			if letter in escape:
				text = text + '\\' + letter
			else:
				text = text + letter
		
		regex = re.compile(text, re.IGNORECASE)
		if not self.tree_iter:
			temp_iter = None
		else:
			temp_iter = self.tree_list.iter_next(self.tree_iter)
		
		for rows in range(len(self.tree_list)):
			if temp_iter == None:
				temp_iter = self.tree_list.get_iter_root()
			else:
				match0 = regex.search(self.tree_list.get_value(temp_iter, 0))
				match1 = regex.search(self.tree_list.get_value(temp_iter, 1))
				if match0 or match1:
					self.update()
					path = self.tree_list.get_path(temp_iter)
					self.tree_selection.select_path(path)
					self.widgets.trParcelle.scroll_to_cell(path)
					break
				else:
					temp_iter = self.tree_list.iter_next(temp_iter)
	
	# Find clear button callback
	def on_find_clear_clicked(self, widget):
		
		self.widgets.entFind.set_text('')
		self.tree_selection.unselect_all()
	
# ---------------------------------------------------------------------------
# 	Start menu and toolbar button callbacks
# ---------------------------------------------------------------------------
	
	# Open menu item callback
	def on_open_clicked(self, widget):
		
		# Back to items page
		self.widgets.notebook.set_current_page(0)
		append = self.options['append']
		
		# Create file selection
		chooser = gtk.FileChooserDialog(title='Open File...', action=gtk.FILE_CHOOSER_ACTION_OPEN, buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL ,gtk.STOCK_OPEN, gtk.RESPONSE_OK))
		icon = chooser.render_icon(gtk.STOCK_OPEN, gtk.ICON_SIZE_MENU)
		chooser.set_local_only(True)
		chooser.set_icon(icon)
		chooser.set_current_folder(os.environ['HOME'])
		# Create and add XML file filter
		xml_filter = gtk.FileFilter()
		xml_filter.set_name('XML Files')
		xml_filter.add_pattern('*.[xX][mM][lL]')
		chooser.add_filter(xml_filter)
		# Run dialog and open selected file
		result = chooser.run()
		path = chooser.get_filename()
		if result == gtk.RESPONSE_OK:
			self.open_document(path, append)
		chooser.destroy()
		self.update()
	
	# Save as menu item callback
	def on_saveas_clicked(self, widget):
		
		# Back to items page
		self.widgets.notebook.set_current_page(0)
		
		# Create file selection
		chooser = gtk.FileChooserDialog(title='Save As...', action=gtk.FILE_CHOOSER_ACTION_SAVE, buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL ,gtk.STOCK_SAVE, gtk.RESPONSE_OK))
		icon = chooser.render_icon(gtk.STOCK_SAVE_AS, gtk.ICON_SIZE_MENU)
		chooser.set_do_overwrite_confirmation(True)
		chooser.set_local_only(True)
		chooser.set_icon(icon)
		chooser.set_current_folder(os.environ['HOME'])
		# Create and add XML file filter
		xml_filter = gtk.FileFilter()
		xml_filter.set_name('XML Files')
		xml_filter.add_pattern('*.[xX][mM][lL]')
		chooser.add_filter(xml_filter)
		# Run dialog and open selected file
		result = chooser.run()
		path = chooser.get_filename()
		if result == gtk.RESPONSE_OK and path:
			temp = str(path[-4:])
			temp = temp.lower()
			if not '.xml' in temp:
				path = path + '.xml'
			self.save_document(path)
		chooser.destroy()
	
	# Quit button/menu item callback
	def on_quit_clicked(self, widget=None):
		
		# Prevent quits if a dialog is open
		if gtk.grab_get_current():
			return
		
		if self.options['remember'] < len(self.tree_list) and self.options['warning'] == True:
			self.tray.show_warning(int(len(self.tree_list)) - int(self.options['remember']))
		else:
			gtk.main_quit()
		
	# Edit button/menu item callback
	def on_edit_clicked(self, widget=None):
		
		if self.tree_iter and self.page == 0:
			self.widgets.notebook.set_current_page(2)
	
	# Copy button/menu item callback
	def on_copy_clicked(self, widget):
		
		if self.tree_iter and self.page == 0:
			text = self.tree_list.get_value(self.tree_iter, 1)
			self.tree_list.set_value(self.tree_iter, 0, datetime.datetime.now().strftime('%m/%d/%y %I:%M%p').lower())
			self.clipboard.set_text(text)
			self.clipboard_text = text
	
	# Delete button/menu item callback
	def on_delete_clicked(self, widget):
		
		if self.tree_iter and self.page == 0:
			# Remove selected from list and select next
			path = self.tree_list.get_path(self.tree_iter)
			self.tree_list.remove(self.tree_iter)
			self.tree_selection.select_path(path)
			# Last was deleted, select new last
			if not self.tree_selection.path_is_selected(path):
 				row = path[0]-1
				# If list not empty select last
				if row >= 0:
					self.tree_selection.select_path((row,))
	
	# Delete key pressed callback
	def on_key_pressed(self, widget, event):
		
		if event.keyval == 65535:
			self.on_delete_clicked(widget)
	
	# Clear button/menu item callback
	def on_clear_clicked(self, widget):
		
		if self.page == 0 and len(self.tree_list) > 0:
			dialog = gtk.MessageDialog(self.widgets.frmMain, 0, gtk.MESSAGE_INFO, gtk.BUTTONS_OK_CANCEL, 'Clear all saved items?')
			icon = dialog.render_icon(gtk.STOCK_CLEAR, gtk.ICON_SIZE_MENU)
			dialog.set_icon(icon)
			answer = dialog.run()
			dialog.destroy()
			if answer == gtk.RESPONSE_OK:
				self.tree_list.clear()
				self.tray.recent.clear()
				self.update()
	
	# Preferences button/menu callback
	def on_preferences_clicked(self, widget=None):
		
		# This helps prevent multiple instances
		if gtk.grab_get_current():
			return
		
		preferences = Preferences(self, PreferencesWidgets())
		result = preferences.run()
		if result:
			self.options = result
			self.clipboard.connect('owner-change', self.clipboard_new_text)
		self.update()
	
	# About menu callback
	def on_about_clicked(self, widget):
		
		# This helps prevent multiple instances
		if gtk.grab_get_current():
			return
		
		about = AboutDialog()
		about.run()
	
# ---------------------------------------------------------------------------
# 	Start main window related callbacks and methods
# ---------------------------------------------------------------------------
	
	# Callback to overwrite default action when window closes
	def on_main_delete(self, widget, event):
		
		self.widgets.frmMain.hide()
		return True
	
	# Removes old items over the limit
	def remove_extra(self):
		
		amount = int(len(self.tree_list)) - int(self.options['remember'])
		if amount > 0:
			self.tree_list.set_sort_column_id(0, gtk.SORT_ASCENDING)
			self.tree_list.sort_column_changed()
			for x in range(0,amount):
				self.tree_list.remove(self.tree_list.get_iter_root())
	
	# Open an XML file
	def open_document(self, path, append=True):
		
		if append == False:
			self.tree_list.clear()
			self.update()
		
		# Try to append the saved history entries to the table
		if os.path.exists(path):
			try:
				document = xml.dom.minidom.parse(path)
				# Append all the items
				items = document.getElementsByTagName('item')
				self.widgets.trParcelle.freeze_child_notify()
				for item in items:
					self.tree_list.append([item.attributes['date'].nodeValue, item.childNodes[0].nodeValue])
				self.widgets.trParcelle.thaw_child_notify()
			except:
				dialog = gtk.MessageDialog(self.widgets.frmMain, 0, gtk.MESSAGE_INFO, gtk.BUTTONS_OK, 'Error opening XML file.')
				icon = dialog.render_icon(gtk.STOCK_PASTE, gtk.ICON_SIZE_MENU)
				dialog.set_icon(icon)
				answer = dialog.run()
				dialog.destroy()
	
	# Save list to an XML file
	def save_document(self, path=common.PARCELLE_FILE):
		
		# Create XML document and append main element
		document = xml.dom.minidom.Document()
		ele_parcelle = document.createElement('parcelle')
		document.appendChild(ele_parcelle)
		
		if not path and self.options['remember'] < len(self.tree_list):
			self.remove_extra()
		ele_parcelle.setAttribute('remember', str(self.options['remember']))
		ele_parcelle.setAttribute('capture', str(self.options['capture']))
		ele_parcelle.setAttribute('append', str(self.options['append']))
		ele_parcelle.setAttribute('warning', str(self.options['warning']))
		ele_parcelle.setAttribute('browse', str(self.options['browse']))
		ele_parcelle.setAttribute('hyperlinks', str(self.options['hyperlinks']))
		
		for row in self.tree_list:
			ele_item = document.createElement('item')
			ele_item.appendChild(document.createTextNode(str(row[1])))
			ele_item.setAttribute('date', row[0])
			ele_parcelle.appendChild(ele_item)
		
		try:	
			xml.dom.ext.PrettyPrint(document, open(path, 'w'))
		except IOError:
			print 'Could not save XML document.'
		
# ---------------------------------------------------------------------------
# 	Start misc methods
# ---------------------------------------------------------------------------
	
	# Check if given text is a hyperlink
	def check_url(self, link=None):
		
		# Match text with URL regular expression
		# FIXME: Update this URL regex
		regex = re.compile('((ht|f)tp(s?)\:\/\/|~/|/)?([\w]+:\w+@)?(([a-zA-Z]{1}([\w\-]+\.)+([\w]{2,6})))((:[\d]{1,5})?)?((/?\w+/)+|/?)([\w\-]+\.[\w]{3,4})?([,]\w+)*((\?\w+=\w+)?(&\w+=\w+)*([,]\w*)*)?', re.IGNORECASE)
		match = regex.match(str(link))
		
		if match:
			# Text is URL
			return True
		else:
			# Text not URL
			return False
